package com.pentajock.docdb.utils;

/**
 * Liscense: 	GPL http://www.gnu.org/copyleft/gpl.html
 * By: 		A. Tres Finocchiaro
 * Date:	2007
 * Java:	6.0SE
 */
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
//import java.net.URI;
import javax.swing.*;
import java.awt.Font;
//import java.awt.Color;
import java.awt.Image;
import java.util.zip.*;
import java.awt.Cursor;
import java.awt.event.*;
//import java.awt.Component;
import java.awt.Dimension;
//import java.awt.FlowLayout;
//import java.awt.TextComponent;
import javax.swing.table.*;
import javax.swing.border.*;
import javax.swing.UIManager.LookAndFeelInfo;
//import java.sql.SQLException;
//import com.jacob.com.Variant;
import com.jacob.com.Dispatch;
import java.text.SimpleDateFormat;
import com.jacob.activeX.ActiveXComponent;
import com.pentajock.docdb.*;
import com.pentajock.docdb.colors.Colors;
import com.pentajock.docdb.database.DBController;
import com.pentajock.docdb.dialogs.Dialogs;
import com.pentajock.docdb.types.User;
import com.pentajock.docdb.images.DBImages;
import com.pentajock.docdb.lang.Lang;
import com.pentajock.docdb.office.Jacob;
import com.pentajock.docdb.types.*;



public class Utils {
	private static boolean overrideBoldFonts = true;

	private static String userRoot = null;
	private static String deptRoot = null;
	private static String docRoot = null;
	private static String appRoot = null;
	private static String doneRoot = null;
	private static String archiveRoot = null;
	//private static String awaitingRoot = null;
	//private static FilenameFilter documentFilter = null;
	
	private static List<XMLDocType> xmlDocTypeCache = new ArrayList<XMLDocType>();
	private static String COLON = Lang.MISC.getText("CHOICE_DELIMITER", ":");
	private static String LOST_AND_FOUND = Lang.MISC.getText("LOST_AND_FOUND", "Lost & Found");
	
	/**
	 * Default constructor - Does nothing
	 */
	private Utils() {
		//
	}
	
	/** 
	 * Data type used to cache's filenames to documentTypes to help performance
	 * Created to improves list response times, especially when listing XML files
	 */
	private static class XMLDocType {
		public String docPath;
		public String docType;
		public XMLDocType(String docPath, String docType) {
			this.docPath = docPath;
			this.docType = docType;
		}
	}

	/** Quick integer tester */
	static public boolean isInt(Object i) {
		try {
			getInt(i);
			return true;
		}
		catch(Exception e) {
		}
		return false;
	}
	
	/**
	 * Quick integer converter
	 */
	static public int getInt(Object i) throws Exception {
		String text;
		if (i instanceof String)
			text = (String)i;
		else if (i instanceof Integer)
			text = "" + ((Integer)i).intValue();
		else
			text = i.toString();
		
		return Integer.parseInt(text);
	}
	
	/**
	 * Tests for a good update interval (>-2)
	 */
	static public boolean goodUpdateInterval(Object i) {
		try {
			return getInt(i)>-2;
		}
		catch (Exception e) {
		}
		return false;
	}
	
	/**
	 * Convenience method for createDocument(File, String)
	 */
	public static Document createDocument(File f, User u) {
		return createDocument(f, u.getUserName());
	}
	
	/* public static Document createDocument(File f, String userName) {
		return createDocument(f, userName, null, null);
	}*/
	
	public static Document createDocument(File docFile, String userName) {
		return createDocument(docFile, userName, null, -2);
	}
	
	/**
	 * Creates a Document object from a File.  Needs userName to retreive department
	 * information.
	 */
	public static Document createDocument(File docFile, String userName, String categoryName, int updateInterval) {
		int documentID = getDocumentID(docFile);
		String departmentName = DBController.getUserDepartment(userName);
		String documentName = getSubject(docFile);
		String extension = getExtension(docFile);
		categoryName = (categoryName == null ? Dialogs.categoryNameDialog(docFile) : categoryName);
		Date date = new Date();
		updateInterval = (updateInterval == -2 ? Dialogs.updateIntervalDialog(categoryName, docFile) : updateInterval);
		String path = (docFile == null? null : docFile.getParent());
		String activeXCall = DBController.getActiveXCall(docFile);
		
		return new Document(documentID, 
			departmentName, 
			documentName, 
			extension, 
			categoryName, 
			date,
			updateInterval, 
			path,
			false,
			null,
			null,
			activeXCall);
	}
	
	/**
	 * Process Main.main() command line 
	 * args[] and return true if program 
	 * should stay running 
	 */
	static public boolean processArgs(String[] args) {
		String[] argSwitch = {"--", "-", "/", "\\"}; // Allowable args prefix
		String iniName = "main.ini"; //Main.getCallerClass(false) + ".ini";
		String lang = "default.ini";
		for (int i = 0; i < args.length; i++) {
			boolean argGood = false;
			for (String s:argSwitch) {
				if (args[i].startsWith(s)) {
					args[i] = args[i].substring(s.length());
					argGood = true; break;
				}
			}
			if (argGood) {
				if (args[i].toLowerCase().startsWith("ini=") && args[i].length() > 4) { 
					iniName = args[i].substring(4); 
				}
				else if(args[i].toLowerCase().startsWith("lang=") && args[i].length() > 5) {
					lang = args[i].substring(5);
				}
				args[i] = args[i].toLowerCase();
			}
			else args[i] = "";
		}
		if (Arrays.asList(args).contains("help") || Arrays.asList(args).contains("?")) {
			printHelp();
			return false;
		}
		Main.setDebugMode(Arrays.asList(args).contains("debug"));
		Utils.checkMandatoryFile("/com/pentajock/docdb/utils/" + iniName);
		Utils.checkMandatoryFile("/com/pentajock/docdb/office/jacob32.dll");
		Main.sysIni = new QuickIni(iniName);
		Main.userIni = new QuickIni(System.getProperty("user.home").toLowerCase() + "/.Document Database/settings.ini");
		String sysLang = Main.sysIni.getStringProperty("Language", "lang");
		String userLang = Main.userIni.getStringProperty("Language", "lang");
		sysLang = (sysLang==null || sysLang.equals(""))?"default.ini":sysLang;
		userLang = (userLang==null || sysLang.equals(""))?"default.ini":userLang;
		Lang.setLanguage(lang.equals("default.ini")?(userLang.equals("default.ini")?sysLang:userLang):lang);
		Colors.departmentColor.setColor(Main.userIni.getStringProperty("Colors", "department_color"));
		Colors.alertColor.setColor(Main.userIni.getStringProperty("Colors", "alert_color"));
		return true;
	}
	
	/**
	 * Categories should have default intervals in the database.
	 * if they don't, who knows what will happen ;)
	 *
	 * This checks the document for an Update Interval (in custom properties),
	 * then bangs against the database if the doc is missing it
	 */
	static public int getBestUpdateInterval(Dispatch oDocument, File docFile, String category) {
		int updateInterval = -2;
		if (oDocument != null)
			 updateInterval = Jacob.getUpdateInterval(oDocument, docFile);
		return updateInterval == -2 ? DBController.getDefaultInterval(category) : updateInterval;
	}
	
	/**
	 * Extensions should have default Categories in the database
	 * if they don't, things could blow up :)
	 *
	 * This checks the document for a Category name (in default properties),
	 * then bangs against the database if the doc is missing it, or if its
	 * not a category found in the database
	 */
	static public String getBestCategoryName(Dispatch oDocument, File f, List<String> categories) {
		String category = null;
		if (oDocument != null) {
			category = Jacob.getCategory(oDocument, f, false);
			if (categories.indexOf(category) == -1)
				category = null;
		}
		if (category == null || category.trim().equals("")) 
			category = DBController.getDefaultCategory(f);
		
		return category;
	}
	
	/**
	 * This attempts to get an "Effective Date" or "Last Modifed" date from
	 * a document with Jacob.
	 *
	 * If Jacob can't find this value, or there's any error opening the document,
	 * it checks the file modified date next.
	 *
	 * If this fails also, it gets a old date (July 10th, 1996). This will
	 * probably make the document expired upon population, forcing its review
	 */
	static public Date getBestEffectiveDate(Dispatch oDocument, File docFile) {
		if (oDocument != null) {
			try {
				return Jacob.getEffectiveDate(oDocument, docFile);
			}
			catch (Exception e) {
				Main.printDebug("(WARNING) Could not get effective date"); 
				//Main.printStackTrace(e, "Could not get Effective Date");
			}
		}
		try {
			return new Date(docFile.lastModified());
		}
		catch (Exception e) {
			Main.printStackTrace(e, "Could not get last modified date");
		}
		return new Date((int)837039928046f);
	}
	
	/**
	 * Get best values for the document after a disk scan.  Similar to 
	 * createDocument but comes pre-filled with some defaults and does some 
	 * essential category and date assumptions, to keep the user from being 
	 * prompted for each file.
	 *
	 * Note: Currently only supports Dispatch references.  OpenOffice has not 
	 *	been included. 
	 */
	static public void prepareDocumentFromScan(Object docRef, Document d, List<String> cats, List<String> depts) {
		Dispatch oDoc;
		if (docRef instanceof Dispatch)
			oDoc = (Dispatch)docRef;
		else
			return;
		
		d.setCategory(Utils.getBestCategoryName(oDoc, d.toFile(), cats));
		d.setUpdateInterval(Utils.getBestUpdateInterval(oDoc, d.toFile(), d.getCategory()));
		d.setEffectiveDate(Utils.getBestEffectiveDate(oDoc, d.toFile()));
		d.setDepartmentName(Utils.getBestDepartmentName(oDoc, d.toFile(), depts));
		d.setApproved(true);
		d.setAssignee(null);
	}
	
	/**
	 * First, try to find the department embedded in the document
	 * Second, try to guess the department based on the folder structure
	 * Third, return as "Lost & Found" 
	 */
	private static String getBestDepartmentName(Dispatch oDocument, File f, List<String> departments) {
		if (DBController.supportsCustomProperties(f)) {
			String departmentName = Jacob.getCustomDocumentProperty(oDocument, "Department");
			if (departmentName != null && !departmentName.trim().equals("") && departments.contains(departmentName)) {
				return departmentName;
			}
		}
		
		for (String s : departments)
			if (f.getPath().toLowerCase().contains(s.toLowerCase()))
				return s;
			
		return "-1";
	}
	
	/** 
	 * Recursively get all files based on a path 
	 * and a file filter, such as ".doc, .rtf", etc.
	 */
	public static Collection<File> listFiles(File directory, FilenameFilter filter, boolean recurse) {
		Vector<File> files = new Vector<File>();
		File[] entries = directory.listFiles();
		for (File entry : entries) {
			if (filter == null || filter.accept(directory, entry.getName()))
				files.add(entry);
			if (recurse && entry.isDirectory())
				files.addAll(listFiles(entry, filter, recurse));
		}
		return files;
	}
	
	/** 
	 * Display help information (To command line)
	 */
	static private void printHelp() {
		String[] helpMessage = {
			"\nLaunches the Document Database application", 
			"Usage: java " + Main.getCallerClass(false) + " [-options]",
			"           (to execute the class)",
			"   or  java -jar jarfile.jar [-options]",
			"           (to execute the jar/zipped file)",
			"",
			"where options include:",
			"    -debug      Enables debug output to the console and to [errLog.txt] file",
			"    -ini        Overide the default location for the ini settings file.  By",
			"                default it will look for an ini matching the main class name.",
			"    -lang       Launch program with specified language file located at /lang.",
			"                      Usage:" + Main.getCallerClass(false) + " -lang=en-US",
			"    -help       Shows this help, then exits.",
			""};
		for (String s:helpMessage) {
			System.out.println(s);
		}
	}
	
	/** Get document ID from db if one doesn't already exist */
	public static int getDocumentID(File docFile) {
		Document d = DBController.docQuery(docFile);
		return (d == null ? -1 : d.getDocumentID());
	}
	
	/**
	 * A quick string parser for grabbing the subject of a file name
	 * also used for other purposes and various data types
	 */
	public static String getSubject(Object o) {
		String s;
		if (o == null)
			return null;
		else if (o instanceof String)
			s = (String)o;
		else if (o instanceof File)
			s = ((File)o).getName();
		else if (o instanceof Document)
			return ((Document)o).getDocumentName();
		else
			return o.toString();
		//if (s.startsWith(".") || !(s.length() > 2) || s == null) return s;
		if (s.contains(".")) return s.substring(0, s.lastIndexOf("."));
		else return s;
	}
	
	/**
	 * A quick string parser for grabbing the extension of a file name
	 * also used for other purposes and various data types
	 */
	public static String getExtension(Object o) {
		String s;
		if (o == null)
			return null;
		else if (o instanceof String)
			s = (String)o;
		else if (o instanceof File)
			s = ((File)o).getName();
		else if (o instanceof Document)
			return ((Document)o).getExtension();
		else
			return o.toString();
		if (s.contains(".")) return s.substring(s.lastIndexOf(".") + 1, s.length());
		else return s;
	}
	
	/**
	 * A hack into the JDK 6.0 Desktop class, written for backwards compatibility
	 * Originally implimented for viewing PDFs, but also works well for Linux, OpenOffice, etc
	 */
	public static void desktopCall(String method, Object o) throws Exception {
		Class[] f = new Class[]{java.io.File.class};
		Class[] u = new Class[]{java.net.URI.class};
		Object obj = Class.forName("java.awt.Desktop").getDeclaredMethod("getDesktop").invoke(null);
		method = method.toLowerCase();
		if (method.equals("open") || method.equals("edit") || method.equals("print"))
			Class.forName("java.awt.Desktop").getDeclaredMethod(method, f).invoke(obj,o);
		else
			Class.forName("java.awt.Desktop").getDeclaredMethod(method, u).invoke(obj,o);
		Main.printDebug("Successfully ran desktop call: " + method + ":" + o.toString());
	}
	
	/**
	 * A hack into the JDK 6.0 Window class, written for backwards compatibility
	 * Shows an icon in the top corner of Dialog windows
	 */
	public static void setIconImage(JDialog j, Image b) {
		try {
			Class.forName("java.awt.Window").getDeclaredMethod("setIconImage", new Class[]{java.awt.Image.class}).invoke(j,b);
		}
		catch (Exception e) {}
	}
	
	
	/**
	 * An attempt to standardize the renderers, so TableRender shares prefs with ListRenderer 
	 */
	public static JLabel rendererHelper(JLabel c, Object value, String departmentName, boolean selected, JComponent list) throws Exception {
		Colors.deriveBestColors(list);
		/* Categorical list items, such as [~Operations~] will be bold and won't appear selected */
		if(value.toString().startsWith("~")) {
			c.setFont(c.getFont().deriveFont(Font.BOLD));
			c.setBackground(list.getBackground());
			c.setForeground(list.getForeground());
			c.setBorder(null);
			return c;
		}
	    
		/* Overides Operating System bold preference and set text + icon depending on type */
		if (overrideBoldFonts)
			c.setFont(c.getFont().deriveFont(Font.PLAIN));
		
		if (value instanceof WeightedDocument) {
			WeightedDocument d = (WeightedDocument)value;
			c.setIcon(DBImages.getMimeType(d, Colors.alertColor.getColor()));
			if (d.getDepartmentName().equalsIgnoreCase(departmentName))
				c.setForeground(Colors.departmentColor.getColor(selected));
			
			if (d.getUpdateInterval() > 0) {
				int days = Utils.daysDue(d);
				c.setText(c.getText() + (days>0?" [" + days + " days overdue]":" [due in " + -days + " days]"));
			}
		}
		else if (value instanceof Document) {
			c.setIcon(DBImages.getMimeType(value));
			if (((Document)value).getNewDocumentName() != null)
				c.setText(((Document)value).getNewDocumentName());
			if (!((Document)value).isApproved())
				c.setForeground(Colors.alertColor.getColor(selected));
			else if (((Document)value).getDepartmentName().equalsIgnoreCase(departmentName))
				c.setForeground(Colors.departmentColor.getColor(selected));
		}
		else if (value instanceof User) {
			c.setIcon(DBImages.getUserIcon(value));
		}
		else if (value instanceof File) {
			c.setIcon(DBImages.getMimeType(value));
			//c.setIcon(DBImages.GENERIC_ICON.getIcon());
			c.setText(((File)value).getName());
			//if (DBController.docExistsInDatabase(value)) 
			//	c.setForeground(Colors.departmentColor.getColor(selected));
		}
		else if (value instanceof DBImages) {
			c.setIcon(((DBImages)value).getIcon());
		}
		else if (value instanceof UIManager.LookAndFeelInfo) {
			c.setText(((UIManager.LookAndFeelInfo)value).getName());
			c.setIcon(DBImages.getLookAndFeelIcon(value));
		}
		else if (value instanceof Colors) {
			Colors t = (Colors)value;
			c.setForeground(t.getColor(selected));
			c.setText(c.getText() + " [" + t.getHex() + "]");
			c.setIcon(DBImages.getAttentionImage(DBImages.COLOR_ICON.getIcon(), 
				t.getColor()));
		}
		else if (value instanceof String) {
			switch (ViewMode.listMode) {
				case DEPARTMENT_MODE:
					if (c.getText().equals("-1"))
						c.setText(LOST_AND_FOUND);
					c.setIcon(DBImages.GROUP_ICON.getIcon());break;
				case EXTENSION_MODE:
					c.setIcon(DBImages.getMimeType(value));break;
				case CATEGORY_MODE:
					c.setIcon(DBImages.CATEGORY_ICON.getIcon());break;
				case FILESERVER_MODE:
					c.setIcon(DBImages.SERVER_ICON.getIcon());break;
				case NULL_MODE:
					c.setIcon(DBImages.CLOCK_ICON.getIcon());break;
				default:
					break;
			}
		}
		else if (value instanceof Date) {
			if (value != null) {
				c.setIcon(DBImages.CLOCK_EXCLAIM_ICON.getIcon());
				c.setText(new SimpleDateFormat("MM/dd/yyyy").format((Date)value));
			}
		}
		return c;
	}
	
	/**
	 * When users log out, roots need to be reset
	 * so that next user logging in will see
	 * correct files.
	 */
	public static void resetRoots() {
		userRoot = null;
		appRoot = null;
		docRoot = null;
		doneRoot = null;
		deptRoot = null;
		archiveRoot = null;
		//awaitingRoot = null;
	}
	
	/** 
	 * Gets user root path (where their documents are stored)
	 * and fixes the path for the home "~" linux wildcard
	 */
	public static String getUserRoot(User u) {
		return getUserRoot(u.getUserName());
	}
	
	/** 
	 * Convenience method for getUserRoot(User)
	 */
	public static String getUserRoot(String user) {
		if(userRoot == null) {
			userRoot = getDeptRoot(user) + "/" + user.toLowerCase() + "/";
		}		
		return fixPath(userRoot);
	}
	
	/** 
	 * Gets all-done path (where documents are sent back)
	 * and fixes the path for the home "~" linux wildcard
	 */
	public static String getDoneRoot(User u) {
		return getDoneRoot(u.getUserName());
	}
	
	/** 
	 * Convenience method for getDoneRoot(User)
	 */
	public static String getDoneRoot(String user) {
		if(doneRoot == null) {
			doneRoot = getDeptRoot(user) + "/" +  Lang.FILE_IO.getText("DONE_FOLDER", "Awaiting Approval") + "/";
		}
		return fixPath(doneRoot);
	}
	
	/** To avoid redundant DBController calls - Only used by Utils - No need for fixPath */
	private static String getDeptRoot(Object o) {
		String user;
		if (o instanceof String) {
			user = (String)o;
		}
		else if (o instanceof User) {
			user = ((User)o).getUserName();
		}
		else {
			return null;
		}
		if(deptRoot == null) {
			deptRoot = getAppRoot() + DBController.getUserDepartment(user);
		}
		return deptRoot;
	}
	
	/** 
	 * Gets deleted/archived path (where deleted documents are sent)
	 * and fixes the path for the home "~" linux wildcard
	 */
	public static String getArchiveRoot() {
		if(archiveRoot == null) {
			archiveRoot = getAppRoot() + "/" +  Lang.FILE_IO.getText("ARCHIVED_ROOT", "Z Archived") + "/";
		}
		return fixPath(archiveRoot);
	}

	/** 
	 * Gets root path of all the departments (essentially, grandparent of user root)
	 * and fixes the path for the home "~" linux wildcard
	 */
	public static String getAppRoot() {
		if(appRoot == null) {
			appRoot = DBController.getUserRoot(System.getProperty("os.name"));
		}
		return fixPath(appRoot);
	}
	
	public static String getDocRoot() {
		if(docRoot == null) {
			return DBController.getDocumentRoot(System.getProperty("os.name"));
		}
		return fixPath(docRoot);
	}
	
	/** Fix home "~" linux wildcard and ensure backslash */
	private static String fixPath(String d) {
		if (d.startsWith("~")) d = System.getProperty("user.home") + d.substring(1, d.length());
		if (!d.endsWith("/")) d = d + "/";
		Main.printDebug("Using path \"" + d + "\"");
		return d;
	}
	
	/** 
	 * Get today's date and return it in the right format 
	 */
	public static String getTodaysDate(String dateFormat) {
		SimpleDateFormat timeFormat = new SimpleDateFormat(dateFormat);
		timeFormat.setTimeZone(TimeZone.getDefault());
		return timeFormat.format(new Date());
	}
	
	/** 
	 * Add months to todays date and return as string 
	 */
	public static String addMonths(int updateInterval, String dateFormat) {
		if (updateInterval == -1) return "Exempt";
		SimpleDateFormat timeFormat = new SimpleDateFormat(dateFormat);
		Calendar tCalendar = Calendar.getInstance();
		Date tDate = new Date();
		Integer year = Integer.parseInt(new SimpleDateFormat(separateDate(dateFormat, "y")).format(tDate));		// Isolate year / month / day
		Integer month = Integer.parseInt(new SimpleDateFormat(separateDate(dateFormat, "M")).format(tDate)) -1;
		Integer day = Integer.parseInt(new SimpleDateFormat(separateDate(dateFormat, "d")).format(tDate));										// Disaster recovery?
		tCalendar.set(year,month,day);
		tCalendar.add(tCalendar.MONTH, updateInterval);
		return timeFormat.format(tCalendar.getTime());
	}
	
	/** 
	 * Quick hack to isolate 'dd', 'yyyy', 'MM', etc 
	 */
	private static String separateDate(String dateFormat, String pattern) {
		for (String i : new String("- / M d y \\ | ( ) ! @ # $ % ^ & * : ; < > ?").split(" ")) {
			if (!i.equals(pattern)) dateFormat = dateFormat.replace(i, "");
		}
		return dateFormat;
	}
	
	/**
	 * Parses the date from the given string, and the given format
	 */
	public static Date parseDate(String dateString, String dateFormat) throws ParseException {
		Calendar cal = Calendar.getInstance();
		if (dateString != null)
			return new SimpleDateFormat(dateFormat).parse(dateString);
		throw new ParseException("Parse Exception manually thrown by parseDate(String, String)", 0);
	}
	
	/**
	 * Cycles through date formats until one works, usefull for document
	 * scans, since you won't know what format fields will be in
	 */
	public static Date parseBestDate(String dateString, String[] dateFormats) {
		for (String dateFormat : dateFormats) {
			try {
				return parseDate(dateString, dateFormat);
			}
			catch (ParseException p) {
				Main.printDebug("(WARNING) Parse Exception parsing date: " + dateString);
			}
		}
		return new Date();
	}
	
	/**
	 * Convience method for parseBestDate(String, String[])
	 */
	public static Date parseBestDate(String dateString, String dateFormat1, String dateFormat2) {
		return parseBestDate(dateString, new String[]{dateFormat1, dateFormat2});
	}
	
	/** 
	 * Returns the number of days due/overdue a Documennt is
	 */
	public static int daysDue(Document d) {
		Date docDate = d.getEffectiveDate();
		if (docDate == null) {
			return 999;	
		}
		Calendar then = new GregorianCalendar();
		then.setTime(docDate);
		then.add(Calendar.MONTH, d.getUpdateInterval());
		/* Return days overdue */
		return (int)((Calendar.getInstance().getTimeInMillis() - then.getTimeInMillis())/86400000.0);
		//return (int)((Calendar.getInstance().getTimeInMillis() - then.getTimeInMillis())/2629800000.0);
	}
	
	/** 
	 * Parse out the ActiveXCall for XML file 
	 */
	public static String getXMLActiveXCall(File f) throws Exception {
		return(getSubject(getXMLDocumentType(f)) + ".Application");
	}
	
	/** 
	 * Quick file parser hack to get the mso 
	 * document type out of an XML file
	 * (Uses xmlDocTypeCache)
	 */
	public static String getXMLDocumentType(Object o) throws Exception {
		if (o == null) return null;
		File f;
		if (o instanceof File)
			f = (File)o;
		else if (o instanceof Document) {
			f = ((Document)o).toFile();
		}
		else 
			return null;
		
		if (!f.exists()) return null;
		for (XMLDocType x : xmlDocTypeCache)
			if (x.docPath.equals(f.getPath()))
				return x.docType;
		
		FileReader reader = new FileReader(f);
		BufferedReader buffer = new BufferedReader(reader);
		String l = "";
		boolean found = false;
		while (l != null) {
			l = buffer.readLine();
			if (l.contains("<?mso-application")) {
				found = true; break;
			}
		}
		buffer.close();	reader.close();
		if (found) {
			int b = l.indexOf("progid=");
			l = l.substring(b, b + l.substring(b).indexOf("?>"));
			String docType = l.substring(l.indexOf("\"") + 1, l.lastIndexOf("\""));
			xmlDocTypeCache.add(new XMLDocType(f.getPath(), docType));
			return docType;
		}
		else Main.printDebug("Could not find a document type for XML file " + f.getPath() + " found " + l);
		return null;
	}
	
	/**
	 * Copies a source file to the destination file location
	 * Convenience method for copyFile(File, File)
	 */
	public static boolean copyFile(String s, String d) throws IOException {
		return copyFile(new File(s), new File(d));
	}
	
	/**
	 * Copies a source file to the destination file location
	 */
	public static boolean copyFile(File fSource, File fDest) throws IOException {
		if (fSource == null || fDest == null || !fSource.exists()) return false;
		InputStream sIn = null;
		OutputStream sOut = null;
		fDest.getParentFile().mkdirs();
		
		int nLen = 0;
		sIn = new FileInputStream(fSource);
		sOut = new FileOutputStream(fDest);
		byte[] bBuffer = new byte[1024];
		while ((nLen = sIn.read(bBuffer)) > 0)
			sOut.write(bBuffer, 0, nLen);
		sOut.flush();
		
		if (sIn != null)
			sIn.close();
		if (sOut != null)
			sOut.close();
		Main.printDebug("File \"" + fSource.getName() + "\" copied to \"" + fDest.getName() + "\" successfully.");
		return fDest.exists();
	}
	
	/** 
	 * Copies a file out of the jar to a physical location.  
	 *    Doesn't need to be private, uses a resource stream, so may have
	 *    security errors if ran from webstart application 
	 */
	public static boolean copyFromJar(String sResource, File fDest) {
		if (sResource == null || fDest == null) return false;
		InputStream sIn = null;
		OutputStream sOut = null;
		File sFile = null;
		try {
			fDest.getParentFile().mkdirs();
		}
		catch(SecurityException e) {
                        Main.printDebug("Security exception building parent directories for " + fDest);
                }
                finally {
                        sFile = new File(sResource);
                }
                
		try {
			int nLen = 0;
			sIn = Main.class.getResourceAsStream(sResource);
			if (sIn == null)
				throw new IOException(Lang.DIALOG.get("JAR_COPY_ERROR", "Input is null!  Error copying from jar")  + 
					"(" + sResource + " to " + fDest.getPath() + ")");
			sOut = new FileOutputStream(fDest);
			byte[] bBuffer = new byte[1024];
			while ((nLen = sIn.read(bBuffer)) > 0)
				sOut.write(bBuffer, 0, nLen);
			sOut.flush();
		}
		catch(IOException ex) {
			Dialogs.showError(ex, sFile, Lang.DIALOG.get("JAR_COPY_ERROR", "Error copying from jar"));
		}
		finally {
			try {
				if (sIn != null)
					sIn.close();
				if (sOut != null)
					sOut.close();
			}
			catch (IOException eError) {
				Main.printStackTrace(eError, "Error closing file streams.\"");
			}
		}
		return fDest.exists();
	}
	
	/**
	 * Convenience method for above copyFromJar(String, String)
	 */
	
	public static boolean copyFromJar(String sResource, String sDest) {
		return copyFromJar(sResource, new File(sDest));
	}
	
	/**
	 * Gets some constants for updating a procedure
	 * based on the User (class) and the Document (class) info
	 * Should eventually return its own data-type 
	 */
	public static String[] getConstants(Document doc, User u) {
		if (u == null) return null;
		String author = doc.getProperty("Author");
		//author = (author == null || author.trim().equals(""))?u.getFullName():author;
		return new String[] {u.getFullName(),				// Updated By
			doc.getDocumentName(u),					// Subject
			author == null ? u.getFullName() : author,		// Author
			getTodaysDate("MM/dd/yyyy"),				// Effective Date
			addMonths(doc.getUpdateInterval(), "MM/dd/yyyy"), 	// Review Date
			doc.getDepartmentName(),				// Department
			doc.getCategory(),					// Category
			DBController.getCompany(doc.getDepartmentName()),	// Company
			DBController.getManager(doc.getDepartmentName())};	// Manager
	}
	
	/**
	 * Gets some constants for updating a procedure
	 * based on Microsoft Word dispatch calls to the COM server
	 * Platform-dependant, and hopefully will be re-written to 
	 * support OpenOffice.org.
	 * 
	 * Uses a link to the file, User (class), and some COM-specific stuff 
	 * Should eventually return its own data-type 
	 */
	public static String[] getConstants(Dispatch oDocument, ActiveXComponent oActiveX, File docFile, User u) {
		if (!DBController.isSupportedFile(docFile) || u == null) return null;
		String[] d = new String[9];
		d[0] = u.getFullName();					// Updated By
		d[1] = getSubject(docFile);				// Subject
		d[2] = d[0];						// Author
		d[3] = getTodaysDate("MM/dd/yyyy");			// Effective Date
		d[4] = "";						// Review Date
		d[5] = u.getDepartmentName();				// Department
		d[6] = null;						// Category
		d[7] = null;						// Update Interval
		
		Document doc = DBController.docQuery(docFile); 
		if (doc != null) {
			Main.printDebug("Document exists in database");
			String subO = doc.getDocumentName(u);
                        String subN = doc.getNewDocumentName();
                        d[1] = subN == null || subN.trim().equals("") ? subO : subN;
			d[4] = addMonths(doc.getUpdateInterval(), "MM/dd/yyyy");
			d[5] = doc.getDepartmentName();
			d[6] = doc.getCategory();
			d[7] = "" + doc.getUpdateInterval();
		}
		else {
			String category = Jacob.getCategory(oDocument, docFile);
			int updateInterval = Dialogs.updateIntervalDialog(category, docFile);
			d[4] = addMonths(updateInterval, "MM/dd/yyyy");
			d[6] = category;
			d[7] = "" + getBestUpdateInterval(oDocument, docFile, category);
		}
		
		String author = Jacob.getAuthor(oDocument, docFile, u);
		if (author !=  null && !author.trim().equals("")) d[2] = author;
		return d;
   	}
	
	/** 
	 * Used to determine if a custom property is a constant to the user,
	 * or a value retreived from the database 
	 */
	static public int checkUserConstants(String s) {
		String[] staticVals = {"Updated By", "Subject", "Author", "Effective Date", "Review Date", "Department", "Category", "Update Interval"};
		int i = -1;
		for (String t : staticVals) {
		    i++;
		    if (s.equalsIgnoreCase(t)) return i;
		}
		return -1;
	}

	
	/**
	 * Install all 3rd Party LookAndFeels based on themes/themes.ini listing.
	 * DBImages will look for an icon in themes >> images folder matching the
	 * name in the themes.ini file. Should only be called once.
	 */
	 
	private static void installLookAndFeels() {
		File themesFile = new File(".themes/themes.ini");
		if (themesFile.exists()) {
			File[] themeJars = themesFile.getParentFile().listFiles();
			/* Use ResourceLoader to add all L&F jars to classPath */
			for (File i : themeJars)
				if (i.getName().endsWith(".jar"))
					ResourceLoader.addFile(i);
			
			QuickIni themesIni = new QuickIni(themesFile.getPath());
			Iterator<String> items = themesIni.getAllSectionNames();			
			while(items.hasNext()) {
				String themeSection = items.next().toString();
				for (String propertyName : themesIni.getAllPropertyNames(themeSection)) {
					String className = themesIni.getStringProperty(themeSection, propertyName).trim();
					try {	
						Utils.class.forName(className);
						UIManager.installLookAndFeel(propertyName, className);
					}
					catch (Exception e) {}
				}
			}
		}
		/* Default system Look & Feel */
		UIManager.installLookAndFeel(new LookAndFeelInfo(Lang.BUTTON.getText("DEFAULT_UI", "Default"), null));
	}

	
	/* 
	 * Apply LookAndFeelInfo specified, update the specified JFrame,
	 * and update appropriate ini file
         */
        
        /**
         * The default OS L&F is loaded if LookAndFeelInfo.getClassName() is 
         * null.  If LookAndFeelInfo (or "o") is null, it simply exits.
         * 
         * The LookAndFeels are stored in the themes folder, and use an ini file
         * for specifying which ones are available to the system.
         * 
         * The application stores a system-wide default (main.ini), and the user
         * may override that with their own. 
         * (home_folder/.DocumentDatabase/settings.ini)
         * 
         * Note that some themes are restricted based on OS/liscensing and 
         * will be disabled in the application.
	 */
	public static boolean applyLookAndFeel(Object o, JFrame j) {
		boolean success = false;
		if (o != null && o instanceof LookAndFeelInfo) {
			LookAndFeelInfo u = (LookAndFeelInfo)o;
			String className = u.getClassName();
			//String suitableName = u.getName();
			try {
				Main.setCursor(j, Cursor.WAIT_CURSOR);
				Main.printDebug("Changing look & feel");
				if (className != null) {
					UIManager.setLookAndFeel(className);
				}
				
				else {
					if (UIManager.getLookAndFeel() != null) {
                                            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                                            className = "";
                                        }
				}
				/* Save to user perferences unless its called by Admin Window */
				if (j instanceof AdminWindow)
					Main.sysIni.setStringProperty("User Interface", "l&f", className);
				else
					Main.userIni.setStringProperty("User Interface", "l&f", className);
				/* Call updateUI on all sub-components of the JFrame */
				SwingUtilities.updateComponentTreeUI(j);
				Dialogs.showSuccess(new Object[] {o}, new String[] {Lang.DIALOG.get("GUI_CHANGE", "Succeeded.  Changes may require restart.")});
				success = true;
			}
			catch (Exception e) {
				Dialogs.showError(e, Lang.DIALOG.get("GUI_ERROR", "Error changing look & feel"));
			}
			Main.setCursor(j, Cursor.DEFAULT_CURSOR);
		}
		return success;
	}
	
	/**
	 * A static method for blocking the Quaqua theme from working with any OS but OSX.
	 * This theme can still be forced from the admin menu, but to do so on non-OSX operating
	 * system would be in violation of Macintosh's artwork.
	 */
	public static boolean checkThemeLiscense(LookAndFeelInfo i) {
		String osName = System.getProperty("os.name").toLowerCase();
		String suitableName = i.getName();
		if (suitableName.startsWith("Quaqua") && !osName.startsWith("mac os x")) {
			return false;
		}
		return true;
	}
	
	/**
	 * Checks to see if a mandatory file exists at the given location.
	 * Accepts a File or a String.  If it doesn't exist, it attempts to
	 * copy it from the jar, so that the program can start.
	 */
	public static boolean checkMandatoryFile(String sourcePath) {
		String userDir = System.getProperty("user.dir") + "/";
                File source = new File(sourcePath);
                File target = new File(userDir + source.getName());
                if (!target.exists()) {
			//if (copyFromJar(f.getName(), f.getPath())) {
                        if (copyFromJar(sourcePath, target)) {
				///System.out.println(f.getName() + ":" + f.getPath());
                                return Dialogs.showSuccess(source, Lang.DIALOG.get("MANDATORY_FILE_SUCCESS", "Successfully created mandatory file") + COLON);
                        }
			else  {
				return Dialogs.showError(source, Lang.DIALOG.get("MANDATORY_FILE_ERROR", "Error creating mandatory file") + COLON);
                        }
		}
		return true;
	}
	
		
	/* 
	 * A convenience method for checkMandatoryFile(File)
	 */
	//public static boolean checkMandatoryFile(String filePath) {
	//	return(checkMandatoryFile(new File(filePath)));
	//}
	
	/** 
	 * Determines the best window size for ClientWindow
	 * based on a few variables.  It first checks the userIni for
	 * stored values.  Next it looks to see if the button panel is
	 * visible.  A visible button panel will ultimately force the
	 * frame to a higher window size.
	 */
	public static Dimension getBestSize(JFrame j, Dimension d) {
		Integer width = (int)d.getWidth();
		Integer height = (int)d.getHeight();
		Dimension newDimension;
		if (j instanceof ClientWindow) {
			Integer newWidth = Main.userIni.getIntegerProperty("View", "client_window_width");
			Integer newHeight = Main.userIni.getIntegerProperty("View", "client_window_height");
			newDimension = new Dimension(newWidth == null?width:newWidth, newHeight == null?height:newHeight);
			
			if (Main.userIni.getBooleanProperty("View", "hide_button_panel"))
				return newDimension;
			else {
				return new Dimension (newWidth < width ? width : newWidth, newHeight < height ? height : newHeight);
			}
		}
		return new Dimension(width, height);
	}
	
	/**
	 * Builds a JMenu full of currently installed themes/Look & Feels
	 */
	public static JMenu buildThemesMenu(final JFrame j) {
		JMenu themeMenu = new JMenu(Lang.MENU.getText("THEMES", "Themes"));
		themeMenu.setMnemonic(Lang.MENU.getMnemonic("THEMES", KeyEvent.VK_T));
		ButtonGroup buttonGroup = new ButtonGroup();
		String currentLaf = UIManager.getLookAndFeel().getName().replaceAll(" ", "_");
		Main.printDebug("Current look and feel:" + currentLaf);
		
		/* Dynamic theme selector */
		themeMenu.setIcon(DBImages.GUI_DEFAULT_ICON.getIcon());
		
		for (final LookAndFeelInfo i : UIManager.getInstalledLookAndFeels()) {
			final JRadioButtonMenuItem tempMenuItem = new JRadioButtonMenuItem();
			tempMenuItem.setAction(new AbstractAction(i.getName().replaceAll("_", " "), DBImages.getLookAndFeelIcon(i)) {
				public void actionPerformed(ActionEvent e) {
					applyLookAndFeel(i, j);
				}
			});
			tempMenuItem.setEnabled(checkThemeLiscense(i));
			if (currentLaf.equalsIgnoreCase(i.getName()))
				tempMenuItem.setSelected(true);
			themeMenu.add(tempMenuItem);
			buttonGroup.add(tempMenuItem);
		}
		return themeMenu;
	}
	
	/**
	 *  Builds a JMenu full of available languages
	 */
	public static JMenu buildLanguageMenu() {
		copyJarFolder("docdb.jar", "lang");
		JMenu langMenu = new JMenu(Lang.MENU.getText("LANGUAGE", "Language"));
		langMenu.setMnemonic(Lang.MENU.getMnemonic("LANGUAGE", KeyEvent.VK_T));
		ButtonGroup buttonGroup = new ButtonGroup();
		String currentLang = Lang.getLanguage();
		Main.printDebug("Current language: " + currentLang);
		
		/* Dynamic language selector */
		langMenu.setIcon(DBImages.LANG_DEFAULT_ICON.getIcon());
		
		for (final File f : new File("lang/").listFiles()) {
			if (!getExtension(f).equalsIgnoreCase("ini"))
				continue;
			final JRadioButtonMenuItem tempMenuItem = new JRadioButtonMenuItem();
			tempMenuItem.setAction(new AbstractAction(Lang.getLanguageName(f), DBImages.getLanguageIcon(f)) {
				public void actionPerformed(ActionEvent e) {
					if (Lang.setLanguage(f.getName())) {
						Dialogs.showSuccess(new Object[] {f}, 
							new String[] {Lang.DIALOG.get("LANGUAGE_CHANGE", 
							"Succeeded.  Changes may require restart.")});
						Main.userIni.setStringProperty("Language", "lang", f.getName());
					}
				}
			});
			if (currentLang.equalsIgnoreCase(getSubject(f)))
				tempMenuItem.setSelected(true);
			langMenu.add(tempMenuItem);
			buttonGroup.add(tempMenuItem);
		}
		return langMenu; 
	}
	
	/**
	 * First, dynamically adds the Jar file to the class path with ResourceLoader
	 * Second, creates a String[] type for the main argument
	 * Third, invoke the main method for the specified class
	 */
	public static void launchClass(String jarPath, String className) throws Exception {
		ResourceLoader.addFile(jarPath); 
		Class[] s = new Class[]{java.lang.String.class}; // For passing args[] to main
		//Object obj = Class.forName(className).getDeclaredMethod("main").invoke(null);
		Class.forName(className).getDeclaredMethod("main", s).invoke(null);
		Main.printDebug("Successfully opened: " + jarPath + ":" + className);
	}
	
	
	/**
	 * Copies an entire folder out of a jar to a physical location.
	 * Uses copyFromJar for each individual file
	 */
	public static boolean copyJarFolder(String jarName, String folderName) {
            String root = "com/pentajock/docdb/";
            
            try {
                ZipFile z = new ZipFile(jarName);
                Enumeration entries = z.entries();
                while (entries.hasMoreElements()) {
                    ZipEntry entry = (ZipEntry)entries.nextElement();
                    String item = entry.getName();
                    boolean isDir = entry.isDirectory();
                    String jarItem = "/" + item;    // Fixes absolue path issues
                    
                    if (item.contains(folderName) && !item.endsWith(".class")) {
                        File f = new File(item.substring(root.length()));
                        Main.printDebug(item);
                        if (isDir) {
                            f.mkdir();
                        }
                        else if (!f.exists()) {
                            if (copyFromJar(jarItem, f.getPath())) {
                                    Main.printDebug("Copied: " + item + " to: " + f.getPath());
                            }
                            else {
                                    Main.printDebug("Error copying: " + item + " to: " + f.getPath());
                            }
                        }
                    }
                }
                return true;
            }
            catch (IOException e) {
                //Main.printStackTrace(e);
                Main.printStackTrace(e, "(WARNING) Didn't copy " + folderName + " from " + jarName);
            }
           // catch (URISyntaxException use) {
           //         Main.printStackTrace(use, "(WARNING) URI Syntax Exception: Jar: " + jarName + " Folder: " + folderName);
           // }

            return false;
	}
	
	/**
	 * Builds a JMenu for application specific colors
	 */
	public static JMenu buildColorsMenu(final JFrame j) {
		Colors[] colorsToList = {Colors.departmentColor, Colors.alertColor};
		JMenu colorsMenu = new JMenu(Lang.MENU.getText("COLORS", "Colors"));
		for (final Colors c : colorsToList) {
			JMenuItem tempMenuItem = new JMenuItem();
			tempMenuItem.setAction(new AbstractAction(c.getName(), DBImages.COLOR_ICON.getIcon()) {
				public void actionPerformed(ActionEvent e) {
					c.showDialog(j);
					Main.userIni.setStringProperty("Colors", c.getFixedName(), c.getHex());
				}
			});
			colorsMenu.add(tempMenuItem);
		}
		colorsMenu.setIcon(DBImages.COLOR_ICON.getIcon());
		return colorsMenu;
	}
	
	/**
	 * Unregisters the zipfldr.dll fixes JFileChooser slowness 
	 * issues. (Should be made optional)
	 */
	public static void applyFileChooserTweak() {
		
		String osName = System.getProperty("os.name").toLowerCase();
		if (!osName.contains("windows"))
			return;
		try {
			Main.printDebug("Unregistering zipfldr.dll to speed up program " +
				"(don't worry, windows will reset this)...&");
			Runtime.getRuntime().exec("regsvr32 /s %windir%/system32/zipfldr.dll");
			Main.printDebug("success");
		}
		catch (Exception e) {
			Main.printDebug("failed");
		}
	}
	
	/** 
	 * Sets theme/look and feel based on a className
	 * className must be a valid fully qualilfied className
	 * but may be nested in a jar file inside the themes folder
	 */
	public static boolean setLookAndFeel(String lafClassName, boolean isDecorated) {
		// Install any extra look & feels
		installLookAndFeels();
		try {
			try {
				UIManager.setLookAndFeel(lafClassName);
				JFrame.setDefaultLookAndFeelDecorated(isDecorated);
				// Helps flickering with Windows OS
				if (isDecorated)
					System.setProperty("sun.awt.noerasebackground", "true");
				return true;
			}
			catch (Exception e) {
				// Try to set default L&F if the above location errors out
				UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
			}
		}
		catch(Exception ex) {
			Main.printStackTrace(ex, "Error setting L&F.");
		}
		return false;
	}
	
	/**
	 * Capitalize the first letter in the word
	 */
	public static String capitalize(String s) {
		if (s.length() > 1)
			return s.substring(0,1).toUpperCase() + s.substring(1);
		else if (s.length() == 1)
			return s.toUpperCase();
		else
			return s;
	}
	
	/** 
	 * List-Friendly name for a File or URL
	 */
	public static String getChildRootName(File f) throws Exception {
		return (f.getName() + " on " + f.getParent());
	}
}